package com.m1.tzolkin.model.board;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.m1.tzolkin.model.Bank;
import com.m1.tzolkin.model.IAction;
import com.m1.tzolkin.model.IBank;
import com.m1.tzolkin.model.ITZCalendar;
import com.m1.tzolkin.model.Season;
import com.m1.tzolkin.model.TZCalendar;
import com.m1.tzolkin.model.TzolkinConstants;
import com.m1.tzolkin.model.exceptions.EndOfGameException;
import com.m1.tzolkin.model.exceptions.IllegalWorkerOperationException;
import com.m1.tzolkin.model.exceptions.NoMovedWorkersException;
import com.m1.tzolkin.model.exceptions.NoPlaceException;
import com.m1.tzolkin.model.exceptions.NoWorkerHereException;
import com.m1.tzolkin.model.exceptions.NotEnoughCornException;
import com.m1.tzolkin.model.exceptions.NotEnoughWorkersException;
import com.m1.tzolkin.model.exceptions.WaitingOperationsException;
import com.m1.tzolkin.model.log.ILog;
import com.m1.tzolkin.model.log.Log;
import com.m1.tzolkin.model.players.ITZPlayer;
import com.m1.tzolkin.model.players.IWorker;
import com.m1.tzolkin.model.util.board.TimedBoard;
import com.m1.tzolkin.model.wheels.IWheel;
import com.m1.tzolkin.model.wheels.WheelEnum;

public class BasicBoard extends TimedBoard<ITZPlayer, ITZCalendar> implements IBasicBoard, TzolkinConstants {
	
	// ATTRIBUTS
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 309361129347825095L;
	private final ILog log;
	private final IBank bank;
	private final Map<WheelEnum, IWheel> wheels;
	private IWorker firstWorker;
	private int waitForTurn;
	private int removedWorkers;
	private int placedWorkers;
	private boolean lateFeeding;
	
	protected final PropertyChangeSupport listeners;
	
	// CONSTRUCTEUR
	
	public BasicBoard(Collection<ITZPlayer> players) {
		super(players, new TZCalendar());
		if (players.size() > MAX_PLAYERS) {
			throw new IllegalArgumentException();
		}
		log = new Log();
		bank = new Bank();
		waitForTurn = 0;
		removedWorkers = 0;
		placedWorkers = 0;
		getPlayersManager().setAleaFirst();
		getPlayersManager().setFirstToCurrent();
		setFirst(getPlayersManager().getFirst());
		wheels = new HashMap<WheelEnum, IWheel>();
		for (WheelEnum w : WheelEnum.values()) {
			wheels.put(w, w.createWheel());
		}
		listeners = new PropertyChangeSupport(this);
	}
	
	// REQUETES
	
	@Override
	public PropertyChangeListener[] getPropertyChangeListeners() {
		return listeners.getPropertyChangeListeners();
	}
	
	@Override
	public ILog getLog() {
		return log;
	}
	
	@Override
	public IWheel getWheel(WheelEnum t) {
		if (t == null) {
			throw new IllegalArgumentException();
		}
		return wheels.get(t);
	}
	
	@Override
	public IBank getBank() {
		return bank;
	}
	
	@Override
	public int getRemovedWorkers() {
		return removedWorkers;
	}
	
	@Override
	public int getPlacedWorkers() {
		return placedWorkers;
	}
	
	@Override
	public IWorker getFirstWorker() {
		if (firstWorker == null) {
			return null;
		}
		return firstWorker;
	}
	
	@Override
	public Season feedingDay() {
		return getTurnsCounter().feedingDay();
	}
	
	@Override
	public int isWaitingForTurn() {
		return waitForTurn;
	}

	@Override
	public int getCorn() {
		return getTurnsCounter().getCorn();
	}

	@Override
	public int getTurns() {
		return getTurnsCounter().getTurns();
	}
	
	// COMMANDES
	
	@Override
	public void addPropertyChangeListener(PropertyChangeListener l) {
		listeners.addPropertyChangeListener(l);
	}
	
	@Override
	public void removePropertyChangeListener(PropertyChangeListener l) {
		listeners.removePropertyChangeListener(l);
	}
	
	@Override
	public ITZPlayer endTurn() throws WaitingOperationsException, EndOfGameException, NoMovedWorkersException {
		if (waitForTurn != 0) {
			throw new IllegalStateException();
		}
		if (removedWorkers == placedWorkers) {
			throw new NoMovedWorkersException();
		}
		removedWorkers = 0;
		placedWorkers = 0;
		// Si c'est le dernier joueur qui finit son tour...
		if (getPlayersManager().getLast() == getPlayersManager().getCurrent()) {
			return beginTurnDay();
		}
		ITZPlayer oldCurrent = getCurrentPlayer();
		getPlayersManager().incCurrent();
		listeners.firePropertyChange("current", oldCurrent, getCurrentPlayer());
		listeners.firePropertyChange("begforcorn", false, getCurrentPlayer().getCorn() < LIMIT_CORN_FOR_ASK);
		return null;
	}
	
	@Override
	public void placeFirstWorker() throws NoPlaceException, NotEnoughCornException, NotEnoughWorkersException, IllegalWorkerOperationException {
		if (waitForTurn != 0) {
			throw new IllegalStateException();
		}
		if (removedWorkers != 0) {
			throw new IllegalWorkerOperationException();
		}
		if (getCurrentPlayer().getHandPawns().size() == 0) {
			throw new NotEnoughWorkersException();
		}
		if (getCurrentPlayer().getCorn() < placedWorkers) {
			throw new NotEnoughCornException();
		}
		if (firstWorker != null) {
			throw new NoPlaceException();
		}
		listeners.firePropertyChange("begforcorn", true, false);
		firstWorker = getCurrentPlayer().leavePawn();
		getCurrentPlayer().removeCorn(placedWorkers);
		getLog().addSentence(new String[] {
			getCurrentPlayer().getName(), "PAY", "" + placedWorkers, "CORN", "AND", "PUT", "1", "WORKER", "ON", "FIRST_PLAYER_CASE"
		});
		++placedWorkers;
		listeners.firePropertyChange("first", null, firstWorker.getOwner());
	}
	
	@Override
	public void placeWorker(WheelEnum w) throws NoPlaceException, NotEnoughCornException, IllegalWorkerOperationException, NotEnoughWorkersException {
		if (w == null) {
			throw new IllegalArgumentException();
		}
		if (waitForTurn != 0) {
			throw new IllegalStateException();
		}
		if (removedWorkers != 0) {
			throw new IllegalWorkerOperationException();
		}
		if (getCurrentPlayer().getHandPawns().size() == 0) {
			throw new NotEnoughWorkersException();
		}
		int cost = getWheel(w).getMinimalPosition() + placedWorkers;
		if (getCurrentPlayer().getCorn() < cost) {
			throw new NotEnoughCornException();
		}
		listeners.firePropertyChange("begforcorn", true, false);
		getWheel(w).placeWorker(getCurrentPlayer().leavePawn());
		getCurrentPlayer().removeCorn(cost);
		getLog().addSentence(new String[] {
			getCurrentPlayer().getName(), "PAY", "" + cost, "CORN", "AND", "PUT", "1", "WORKER", "ON", w.toString(), "" + (cost - placedWorkers)
		});
		++placedWorkers;
	}
	
	@Override
	public Map<IAction, Integer> takeWorker(WheelEnum w, int position) throws IllegalWorkerOperationException, NoWorkerHereException {
		if (w == null) {
			throw new IllegalArgumentException();
		}
		if (waitForTurn != 0) {
			throw new IllegalStateException();
		}
		if (placedWorkers != 0) {
			throw new IllegalWorkerOperationException();
		}
		IWorker worker = getWheel(w).getWorker(position);
		if (worker == null || worker.getOwner() != getCurrentPlayer()) {
			throw new NoWorkerHereException();
		}
		listeners.firePropertyChange("begforcorn", true, false);
		++removedWorkers;
		getLog().addSentence(new String[] {
			getCurrentPlayer().getName(), "TAKE", "1", "WORKER", "FROM", w.toString(), "" + position
		});
		return getWheel(w).removeWorker(position);
	}

	@Override
	public void emptyCorn() {
		int oldCorn = getTurnsCounter().getCorn();
		getTurnsCounter().emptyCorn();
		listeners.firePropertyChange("corn", oldCorn, getTurnsCounter().getCorn());
	}
	
	@Override
	public void addCorn() {
		int oldCorn = getTurnsCounter().getCorn();
		getTurnsCounter().addCorn();
		listeners.firePropertyChange("corn", oldCorn, getTurnsCounter().getCorn());
	}

	@Override
	public void incTurn() throws EndOfGameException, WaitingOperationsException {
		incTurn(waitForTurn);
	}
	
	@Override
	public void incTurn(int d) throws EndOfGameException, WaitingOperationsException {
		if (waitForTurn == 0) {
			throw new IllegalStateException();
		}
		if (d > MINIMUM_DAY_TURN && firstWorker != null && !firstWorker.getOwner().canTurnTwice()) {
			throw new WaitingOperationsException();
		}
		if (firstWorker != null && d > MINIMUM_DAY_TURN) {
			firstWorker.getOwner().setTurnTwice(false);
		}
		// Gestion des jours de festin => nourrir les ouvriers
		if (lateFeeding || getTurnsCounter().feedingDay() != null) {
			for (ITZPlayer p : getPlayers()) {
				p.feedWorkers(getLog());
			}
			lateFeeding = false;
		}
		// Tour des roues
		for (int i = 0; i < d; ++i) {
			if (i > 0 && getTurnsCounter().feedingDay() != null) {
				lateFeeding = true;
			}
			for (WheelEnum w : wheels.keySet()) {
				wheels.get(w).turnDay();
			}
			getTurnsCounter().incTurn();
		}
		if (d > MINIMUM_DAY_TURN) {
			getLog().addSentence(new String[] {
				firstWorker.getOwner().getName(), "CHOOSE", "TO", "ADVANCE", "2", "DAYS"
			});
		}
		waitForTurn = 0;
		if (firstWorker != null) {
			ITZPlayer p = firstWorker.getOwner();
			if (p == getFirstPlayer()) {
				getPlayersManager().setFirstToNeighbour();
			} else {
				getPlayersManager().setFirst(p);
			}
			setFirst(getPlayersManager().getFirst());
			p.backPawn(firstWorker);
			firstWorker = null;
			listeners.firePropertyChange("first", null, null);
		}
		getPlayersManager().setFirstToCurrent();
		listeners.firePropertyChange("current", null, getCurrentPlayer());
		listeners.firePropertyChange("begforcorn", false, getCurrentPlayer().getCorn() < LIMIT_CORN_FOR_ASK);
	}
	
	// OUTILS
	
	/**
	 * Donne le jeton Premier Joueur au Premier Joueur.
	 */
	private void setFirst(ITZPlayer p) {
		for (ITZPlayer pl : getPlayers()) {
			pl.setFirstPlayer(p == pl);
		}
	}

	/**
	 * Entame les proc�dures de tournage de la grande roue.
	 */
	private ITZPlayer beginTurnDay() throws WaitingOperationsException, EndOfGameException {
		// Gestion de l'emplacement Premier Joueur
		ITZPlayer p = null;
		if (firstWorker != null) {
			p = firstWorker.getOwner();
			p.addCorn(getCorn());
			emptyCorn();
		}
		// S'il y avait un joueur sur l'emplacement Premier Joueur
		if (p != null && p.canTurnTwice()) {
			waitForTurn = (canMultipleTurns(MAXIMUM_DAY_TURN)) ? MAXIMUM_DAY_TURN : MINIMUM_DAY_TURN;
			listeners.firePropertyChange("multipleTurn", 0, waitForTurn);
		} else {
			waitForTurn = MINIMUM_DAY_TURN;
			addCorn();
			incTurn();
		}
		return p;
	}
	
	/**
	 * Retourne true si toutes les roues peuvent tourner de i tours.
	 */
	private boolean canMultipleTurns(int i) {
		for (WheelEnum w : wheels.keySet()) {
			if (!wheels.get(w).canMultipleTurn(i)) {
				return false;
			}
		}
		return true;
	}

}
